一份 JavaScript 中“二进制生态”极简内功心法
诙谐版 · 但保证概念不缩水
0. 前言:为什么我们要跟二进制“死磕”?
前端的世界看似是字符串、JSON 和 DOM 的乐园,但当你遇到:
- 上传一张图片要预览、要切片
- 下载一个文件要命名、要触发保存
- WebSocket 收到一段二进制协议(比如游戏对战数据)
- 从 Canvas 抠出像素自己加工
- 处理一个 ZIP 文件、解析一个 PDF、做 Web Audio 可视化
……你就会发现:二进制数据才是幕后真正的大佬。
JavaScript 为我们准备了一整套二进制“兵器库” —— 从最底层的 ArrayBuffer,到最上手的 Blob,再到流、文件、Base64……
它们听起来像一群近亲,很容易搞混。今天我们就从底层到顶层,把整个生态撸一遍。
读完你会明白:为什么有这么多概念?它们各自解决什么问题?以及——到底什么时候用哪个?
1. 一切的基础:ArrayBuffer —— “一块干净的内存荒地”
想象你向操作系统申请了一块 连续的内存,大小是 16 字节。这块内存里全是 0 和 1,没有类型,没有结构。
const buffer = new ArrayBuffer(16);
console.log(buffer.byteLength); // 16
重要特征:
- 长度固定,不能直接扩容(但可以拷贝生成新的)。
- 你不能直接读写它 —— 就像一个没有门牌号的荒地区域,JS 不让你直接往里踩。
为什么这么设计?
因为原始二进制数据没有“类型”概念,如果直接操作很容易出现字节序错乱、类型误用。JavaScript 作为动态语言,选择强制你通过“视图”来操作,既安全又灵活。
于是,我们需要 视图(View) 来给这片荒地铺上“地板砖”。
2. 视图一:TypedArray —— “铺上统一尺寸的地砖”
TypedArray 不是单个类,而是一族类:Uint8Array、Int16Array、Float32Array …… 它们把 ArrayBuffer 当作一个 数字数组 来读写,每个元素的大小是固定的。
const buffer = new ArrayBuffer(8);
const uint8 = new Uint8Array(buffer); // 每块砖 1 字节,共 8 块
const int32 = new Int32Array(buffer); // 每块砖 4 字节,共 2 块
uint8[0] = 0xFF;
uint8[1] = 0x12;
console.log(int32[0]); // 猜猜输出?小端序环境下是 0x12FF(即 4863)
不同视图可以 共享同一块内存,这非常强大 —— 你可以把同一段二进制同时当作字节流和整数数组来操作。
常用 TypedArray 一览:
| 类型 | 元素大小 | 用途举例 |
|---|---|---|
Uint8Array |
1 | 原始字节、图片数据、文件分片 |
Int8Array |
1 | 有符号小整数 |
Uint16Array |
2 | 像素(RGBA 中的 RGB 常见 16 位) |
Int32Array |
4 | 常规整数运算 |
Float32Array |
4 | WebGL 顶点、音频样本 |
直接创建方式(自动分配 buffer):
const arr = new Uint8Array([0, 1, 2, 3]);
console.log(arr.buffer); // 底层 ArrayBuffer
灵魂拷问:TypedArray 和普通数组有什么区别?
- 普通数组可以存不同类型,动态扩容;TypedArray 是固定类型、固定长度、内存连续,性能极高。
- TypedArray 没有
push、pop等会改变长度的方法。
3. 视图二:DataView —— “万能遥控器,还能切换字节序”
如果你觉得 TypedArray 太“死板”(只能统一类型),或者你需要处理 不同字节序(大端/小端)的数据,请找 DataView。
DataView 可以让你在同一个 ArrayBuffer 上,随意读写任意类型的数据,并且 显式指定字节序。
const buffer = new ArrayBuffer(4);
const dv = new DataView(buffer);
// 大端序写入 0x12345678
dv.setUint32(0, 0x12345678, false);
// 小端序读取前 2 字节作为 Uint16
console.log(dv.getUint16(0, true)); // 小端序 -> 0x5678
字节序(Endianness)是啥?
想象数字0x12345678在内存中摆放:大端序是12 34 56 78(网络传输常用),小端序是78 56 34 12(x86 常用)。DataView让你随心所欲。
什么时候用 DataView?
- 解析二进制文件格式(PNG、MP4、ZIP 等),它们往往混用不同大小的整数。
- 处理网络协议(TCP、WebSocket 自定义协议)。
- 任何需要跨平台交换二进制数据的场景。
4. 进阶:SharedArrayBuffer —— “两个线程抢同一块荒地”
普通 ArrayBuffer 是线程隔离的。如果你在主线程创建一个 buffer,传给 Web Worker,Worker 会得到它的 副本 —— 内存被复制,效率低,且无法同步修改。
SharedArrayBuffer 则允许多个线程 共享同一块内存,配合 Atomics 对象实现原子操作,避免数据竞争。
// 主线程
const sharedBuffer = new SharedArrayBuffer(1024);
const sharedArray = new Int32Array(sharedBuffer);
worker.postMessage(sharedBuffer); // 传的是引用,不是拷贝
// Worker 线程
self.onmessage = (e) => {
const arr = new Int32Array(e.data);
Atomics.add(arr, 0, 1); // 原子加 1
Atomics.notify(arr, 0, 1); // 通知主线程
};
注意:
SharedArrayBuffer需要网站开启 跨域隔离(设置 COOP/COEP 响应头),否则会被浏览器禁用(因为 Spectre 漏洞)。
所以普通业务里用得少,但高性能计算、多线程渲染会用。
5. 把二进制“穿件衣服”:Blob 和 File
ArrayBuffer 太底层了,它不知道什么是“图片”、什么是“文本文件”。Blob 在它之上加了一层 元信息:MIME 类型(type),并且 不可变(immutable)。
const blob = new Blob(['<h1>Hello</h1>'], { type: 'text/html' });
console.log(blob.size, blob.type); // 16, "text/html"
Blob 的妙用:
- 生成临时 URL:
URL.createObjectURL(blob)用于<img>、<video>、<a download>等。 - 切片上传:
blob.slice()实现大文件分片。 - 转成
ArrayBuffer或文本:await blob.arrayBuffer()或await blob.text()。
File 是 Blob 的子类,多了 name、lastModified 属性,通常来自 <input type="file">。
const file = fileInput.files[0];
console.log(file.name, file instanceof Blob); // true
为什么需要 Blob?
因为浏览器需要一种能代表“文件”的对象,并且可以安全地在主线程和 Worker 间传递(结构化克隆算法支持 Blob,而 ArrayBuffer 也可以传递但会转移所有权)。Blob 还能利用浏览器缓存、磁盘存储等。
6. 字符串与二进制之间的摆渡人:TextEncoder / TextDecoder
你肯定遇到过这种需求:把一个字符串变成二进制(比如发送到 WebSocket),或者反过来。
用 TextEncoder 和 TextDecoder,它们专门处理 UTF-8 编码。
const encoder = new TextEncoder();
const uint8 = encoder.encode('你好🌍'); // Uint8Array(12)
const decoder = new TextDecoder('utf-8');
const backToString = decoder.decode(uint8); // "你好🌍"
为什么不用
unescape/encodeURIComponent那些老古董?
那些是历史包袱,性能差且不支持全部 Unicode。TextEncoder是标准现代 API,浏览器和 Node.js 都支持。
注意:TextEncoder 只支持 UTF-8,而 TextDecoder 可以指定其他编码(如 gbk,但需要浏览器支持)。
7. Node.js 中的特殊存在:Buffer
在 Node.js 里,Buffer 是二进制数据的“亲儿子”。它类似于 Uint8Array,但多了许多便利方法:toString、write、from 等。
const buf = Buffer.from('Hello', 'utf8'); // <Buffer 48 65 6c 6c 6f>
buf.write('Hi', 0);
console.log(buf.toString()); // "Hillo"
// Buffer 和 Uint8Array 可以无缝互转
const uint8 = new Uint8Array(buf);
const buf2 = Buffer.from(uint8);
区别:
Buffer是 Node.js 独有的,浏览器没有。Buffer的方法更贴近文件/网络操作(比如buf.slice()的行为不同——它返回新 Buffer 但指向同一段内存,而 TypedArray slice 会复制)。- 现代 Node.js 也完全支持
Uint8Array,但Buffer依然更常用。
设计原因:Node.js 早期就引入了
Buffer,后来浏览器有了Uint8Array,Node 为了兼容和性能保留了Buffer,并且让两者共享内存。
8. 当二进制遇到“大块头”:Streams API
ArrayBuffer 和 Blob 都是一次性加载到内存。如果文件有几百 MB 甚至 GB,页面会卡死。
Streams API 让你像流水一样处理数据:边读边处理,内存占用极低。
// 从 Blob 获取流
const blob = new Blob(['a'.repeat(1024 * 1024)]);
const readableStream = blob.stream();
const reader = readableStream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log('chunk size:', value.length); // value 是 Uint8Array
}
更有用的是 fetch 响应流:
const response = await fetch('https://example.com/bigfile.bin');
const reader = response.body.getReader(); // ReadableStream<Uint8Array>
// 逐步读取...
流还可以 管道 到 WritableStream(如文件写入)、TransformStream(如实时压缩/解压)。
何时用流?
- 大文件上传/下载进度条
- 实时音视频处理
- 解析超大的 JSON/CSV(配合
JSON.parse的流式库)
9. 表单上传专用选手:FormData
当你要上传文件(+ 其他字段)时,FormData 是最省心的方式。它会自动构建 multipart/form-data 请求体,并且可以添加 Blob 或 File。
const formData = new FormData();
formData.append('username', 'alice');
formData.append('avatar', fileBlob, 'avatar.png');
fetch('/upload', { method: 'POST', body: formData });
注意:FormData 内部不会把文件读成字符串或 Buffer,而是直接以二进制分块形式发送,非常高效。
10. 文本化的二进制:Base64
Base64 是用 ASCII 字符表示二进制数据的方式,它把每 3 个字节变成 4 个可打印字符。常用于:
- DataURL:
data:image/png;base64,xxxx - 在 JSON 里传输小文件(比如缩略图)
- 某些旧 API 只接受字符串
浏览器内置函数:btoa(binary to ASCII)和 atob(反向)。
但是它们只接受“二进制字符串”(每个字符的码点 0-255),所以需要先转换:
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
function base64ToArrayBuffer(base64) {
const binary = atob(base64);
const buffer = new ArrayBuffer(binary.length);
const view = new Uint8Array(buffer);
for (let i = 0; i < binary.length; i++) {
view[i] = binary.charCodeAt(i);
}
return buffer;
}
注意:Base64 会使体积膨胀约 33%,不适合大文件。
11. 一张图理清所有关系(灵魂手绘版)
┌─────────────────┐
│ 二进制数据宇宙 │
└────────┬────────┘
│
┌─────────────────────┼─────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌────────────┐ ┌────────────┐
│ArrayBuffer│ │SharedArrayBuffer│ │ Blob │
│(原始内存块)│ │(多线程共享内存)│ │(带MIME+不可变)│
└────┬─────┘ └────────────┘ └─────┬──────┘
│ │
┌────┴────┐ 子类│
▼ ▼ ▼
TypedArray DataView ┌──────┐
(固定类型) (灵活+字节序) │ File │
│ └──────┘
│ Node.js 版
▼
Buffer
字符串 ↔ 二进制:TextEncoder / TextDecoder
流式处理:ReadableStream / WritableStream
表单上传:FormData
文本化:Base64 (btoa/atob)
12. 实战决策指南:我到底该用哪个?
| 你的需求 | 推荐工具 | 为什么 |
|---|---|---|
| 需要逐字节修改二进制数据(如加密、像素处理) | Uint8Array 或 DataView |
直接操作底层内存,性能好 |
| 需要控制字节序(大端/小端) | DataView |
TypedArray 固定使用 CPU 字节序 |
| 多线程共享大块数据并同步 | SharedArrayBuffer + Atomics |
避免拷贝,但要注意安全头 |
| 预览图片、视频,触发下载 | Blob + URL.createObjectURL |
生成本地 URL,浏览器原生支持 |
| 大文件分片上传 | Blob.slice() + FormData |
切片再组包,不卡主线程 |
| 处理用户选中的文件 | File (继承自 Blob) |
自带文件名等信息 |
| 字符串 ↔ UTF-8 二进制 | TextEncoder / TextDecoder |
标准、简洁、全覆盖 Unicode |
| Node.js 中读写文件或 socket | Buffer |
功能丰富,历史正统 |
| 超大文件处理(几百 MB+) | ReadableStream |
内存可控,支持背压 |
| 传统表单 + 文件上传 | FormData |
浏览器自动处理 multipart |
| 在 JSON 或 CSS 里嵌入小图片 | Base64(配合 Uint8Array) |
虽膨胀但方便 |
13. 最后一个段子:它们之间的关系就像……
- ArrayBuffer:买了一块空地(内存),上面啥也没有。
- TypedArray:铺上统一规格的地砖(比如 1m×1m 的方砖),可以走来走去(读写)。
- DataView:一个能随时切换地板材质和形状的万能工具,还能让你决定“瓷砖缝隙朝哪边”(字节序)。
- Blob:给空地拍了一张照片,还标注了“这是花园”还是“这是停车场”(MIME 类型),照片不可修改,但可以复制裁剪。
- File:Blob 照片加上了一个相框,上面写着“我家后院.jpg”和拍摄时间。
- TextEncoder/Decoder:空地和人类语言之间的翻译官。
- Streams:在空地和目的地之间修了一条传送带,一边扔砖头一边在另一头收货。
- Base64:把空地拍成照片后,又把照片印在了一件 T 恤上,虽然 T 恤变大了,但可以贴在任何地方(比如 JSON)。
结尾
JavaScript 的二进制生态看似庞杂,实则每个角色都对应一个 特定的抽象层次和场景。从最底层的 ArrayBuffer 到最上层的 Blob 和 File,再到流和文本编码,它们共同构成了前端处理“非文本数据”的完整能力。
希望这份文档能帮你和你的同事们 少一点“怎么又是个新对象”的困惑,多一点“原来如此”的会心一笑。
最后,一句记忆口诀:
内存裸地 ArrayBuffer,铺砖 TypedArray 和 DataView;
拍照穿 MIME 是 Blob,文件相册 File 承;
字符串过河找 Encoder,大块头走 Stream;
Node 亲儿叫 Buffer,表单上传 FormData;
文本化装 Base64,多线程共享 Shared。
祝你在二进制的海洋里,乘风破浪,永不 leak memory